// TODO: CODE REVIEW & CLEANUP!
//
// LutronDriver.cs
//
// Implements HomeUX.Components.LutronDriver.
//
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Xml;
using HomeUX.DriverModel;
using HomeUX.Networking;
using HomeUX.Synchronization;
using HomeUX.Utilities;

namespace HomeUX.Components
{

    /* TODO: format:
 
      SyncPath schema:
        -- "<device>\Keypads\<keypad-name>\Buttons\<button-id>" = LED state.
		   Supports verb "Press".
    */

/// <summary>
/// Implements a HomeUX driver for the Lutron "Home Works Interactive" system.
/// </summary>
///
public class LutronDriver : Driver
{

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private Fields
    //

    /// <summary>
    /// The serial port specification string that defines the virtual or real serial port to open.
    /// </summary>
    string _serialPortSpec;

    /// <summary>
    /// Provides device-specific hardware access.
    /// </summary>
    LutronHardware _hardware;

    /// <summary>
    /// Maps the 1-based processor identifier of each processor unit to the corresponding
	/// <r>Processor</r> object.
	/// </summary>
	Dictionary<int, LutronProcessor> _mapIdToProcessor = new Dictionary<int, LutronProcessor>();

    /// <summary>
    /// Maps a keypad name (e.g. "Pantry Wall") to the corresponding <r>LutronKeypad</r> object.
    /// </summary>
    Dictionary<string, LutronKeypad> _mapNameToKeypad = new Dictionary<string, LutronKeypad>();

    /// <summary>
    /// Maps a normalized dot-form keypad address (e.g. "1.4.1") to the corresponding
    /// <r>LutronKeypad</r> object.
    /// </summary>
    Dictionary<string, LutronKeypad> _mapAddressToKeypad = new Dictionary<string, LutronKeypad>();

    /// <summary>
    /// Maps a load name to the corresponding <r>LutronLoad</r> object.
    /// </summary>
    Dictionary<string, LutronLoad> _mapNameToLoad = new Dictionary<string, LutronLoad>();

    /// <summary>
    /// Maps a <r>SyncPath</r> of the form
    /// "<i>device></i>\Keypads\<i>keypad-name</i>\Buttons\<i>button-id></i>" to the corresponding
    /// <r>LutronButton</r> object.
    /// </summary>
    Dictionary<SyncPath, LutronButton> _mapPathToButton = new Dictionary<SyncPath, LutronButton>();

    ///////////////////////////////////////////////////////////////////////////////////////////////
	// Public Methods
	//

    // move to protected:
    protected override void OnInitialize()
    {
        // read attributes of the "<Driver>" XML element
        _serialPortSpec = Configuration.GetRequiredAttribute("SerialPort");

		// read child XML elements
        foreach (ConfigurationElement element in Configuration.Children)
        {
            switch (element.Name)
            {

            case "Processor":

                CreateProcessor(element);
                break;

            }
        }
    }

    void CreateProcessor(ConfigurationElement element)
    {
        var processor = new LutronProcessor()
        {
            Driver = this,
            Id = element.GetRequiredIntAttribute("Id"),
        };

        try
        {
            _mapIdToProcessor.Add(processor.Id, processor);
        }
        catch (ArgumentException)
        {
            throw new ConfigurationException(element, "Duplicate Lutron processor identifier: {0}",
                processor.Id);
        }

        foreach (ConfigurationElement childElement in element.Children)
        {
            switch (childElement.Name)
            {

            case "KeypadLink":

                CreateKeypadLink(childElement, processor);
                break;

            case "RpmLink":

                CreateRpmLink(childElement, processor);
                break;

            case "VareoLink":

                CreateVareoLink(childElement, processor);
                break;

            }
        }
    }

    void AddLink(LutronProcessor processor, LutronLink link, ConfigurationElement element)
    {
        try
        {
            processor.MapIdToLink.Add(link.Id, link);
        }
        catch (ArgumentException)
        {
            throw new ConfigurationException(element, "Duplicate Lutron link identifier: {0}",
                link.Id);
        }
    }

    void CreateKeypadLink(ConfigurationElement element, LutronProcessor processor)
    {
        var link = new KeypadLink()
        {
            Processor = processor,
            Id = element.GetRequiredIntAttribute("Id"),
        };
        AddLink(processor, link, element);

        foreach (ConfigurationElement childElement in element.Children)
        {
            if (childElement.Name == "Keypad")
				CreateKeypad(childElement, link);
        }
    }

	void CreateKeypad(ConfigurationElement element, KeypadLink link)
	{
		var keypad = new LutronKeypad()
		{
			Link = link,
			Id = element.GetRequiredIntAttribute("Id"),
			Name = element.GetRequiredAttribute("Name")
		};

		try
		{
			link.MapIdToKeypad.Add(keypad.Id, keypad);
		}
		catch (ArgumentException)
		{
			throw new ConfigurationException(element, "Duplicate Lutron keypad identifier: {0}",
				keypad.Id);
		}

		try
		{
			_mapNameToKeypad.Add(keypad.Name, keypad);
		}
		catch (ArgumentException)
		{
			throw new ConfigurationException(element, "Duplicate Lutron keypad name: {0}",
				keypad.Name);
		}

        _mapAddressToKeypad.Add(keypad.Address, keypad);

		string buttonsString = element.GetRequiredAttribute("Buttons");
		for (int buttonId = 1; buttonId <= buttonsString.Length; buttonId++)
		{
			char buttonChar = buttonsString[buttonId - 1];
			LedBehavior ledBehavior;
			switch (buttonChar)
			{
					//ULETC
			case '-':
				continue;
			case '?':
				ledBehavior = LedBehavior.Unknown;
				break;
			case 'L':
				ledBehavior = LedBehavior.Light;
				break;
			case 'E':
				ledBehavior = LedBehavior.Extinguish;
				break;
			case 'T':
				ledBehavior = LedBehavior.Toggle;
				break;
			case 'C':
				ledBehavior = LedBehavior.Cycle;
				break;
			default:
				throw new ConfigurationException(element,
					"Unexpected character \"{0}\" in \"Buttons\" attribute; expecting one of: - ? L E T C",
					buttonChar);
			}

			var button = new LutronButton()
			{
				Keypad = keypad,
				Id = buttonId,
				LedBehavior = ledBehavior,
                LedIsOn = false
			};

			keypad.MapIdToButton.Add(button.Id, button);
            _mapPathToButton.Add(button.Path, button);
		}
	}

    void CreateRpmLink(ConfigurationElement element, LutronProcessor processor)
    {
        var link = new RpmLink()
        {
            Id = element.GetRequiredIntAttribute("Id")
        };

        foreach (ConfigurationElement childElement in element.Children)
        {
            if (childElement.Name == "Load")
				CreateLoad(childElement, link);
        }
    }

    void CreateVareoLink(ConfigurationElement element, LutronProcessor processor)
    {
        var link = new VareoLink()
        {
            Id = element.GetRequiredIntAttribute("Id")
        };

        foreach (ConfigurationElement childElement in element.Children)
        {
            if (childElement.Name == "Load")
                CreateLoad(childElement, link);
        }
    }

    static void ValidateNormalizedAddress(string address, int partCount, ConfigurationElement element)
    {
        string[] parts = address.Split('.');

        if (parts.Length != partCount)
        {
            throw new ConfigurationException(element, "Expecting a {0}-part Lutron address, got: {1}",
                partCount, address);
        }

        foreach (string part in parts)
        {
            try
            {
                XmlConvert.ToInt32(part);
            }
            catch (Exception ex)
            {
                if ((ex is FormatException) || (ex is OverflowException))
                {
                    throw new ConfigurationException(element, "Invalid Lutron address: {0}", address);
                }
                else
                    throw;
            }
        }
    }

    void CreateLoad(ConfigurationElement element, LoadLink link)
    {
        var load = new LutronLoad()
        {
            Link = link,
            Id = element.GetRequiredAttribute("Id"),
            Name = element.GetRequiredAttribute("Name"),
            Level = 0
        };

        ValidateNormalizedAddress(load.Id, 3, element);

        try
        {
            link.MapIdToLoad.Add(load.Id, load);
        }
        catch (ArgumentException)
        {
            throw new ConfigurationException(element, "Duplicate Lutron load identifier: {0}",
                load.Id);
        }

        try
        {
            _mapNameToLoad.Add(load.Name, load);
        }
        catch (ArgumentException)
        {
            throw new ConfigurationException(element, "Duplicate Lutron load name: {0}",
                load.Name);
        }
    }

    protected override void OnEnable()
    {
        QueueRequest(new LutronEnableRequest() { Driver = this }, false);
    }

    protected override void OnDisable()
    {
        // cancel all parallel requests
        while (_parallelRequests.Count > 0)
            _parallelRequests[0].OnExit();

		CloseHardware();
        DisableCompleted(); // disable completed synchronously
    }

    // can throw VirtualSerialPortException on error
    void OpenHardware()
    {
        VirtualSerialPort serialPort = null;
        try
        {
            serialPort = VirtualSerialPort.GetPort(_serialPortSpec);
            _hardware = new LutronHardware(WorkQueue, serialPort, Name);

            // IMPORTANT: do corresponding "-=" in CloseHardware
            _hardware.CommandFailed += OnHardwareCommandFailed;
            _hardware.CommandTimedOut += OnHardwareCommandTimedOut;
            _hardware.NotificationReceived += OnHardwareNotificationReceived;
            _hardware.WarningsReported += OnHardwareWarningsReported;
            // IMPORTANT: do corresponding "-=" in CloseHardware

            _hardware.Open();
        }
        finally
        {
            // if <serialPort> was allocated but ownership of it wasn't transferred to
            // <_hardware>, free <serialPort> to prevent a memory leak
            if ((serialPort != null) && (_hardware == null))
            {
                serialPort.Dispose();
                serialPort = null;
            }
        }

    }

	void CloseHardware()
	{
		_hardware.CommandFailed -= OnHardwareCommandFailed;
		_hardware.CommandTimedOut -= OnHardwareCommandTimedOut;
		_hardware.NotificationReceived -= OnHardwareNotificationReceived;
		_hardware.WarningsReported -= OnHardwareWarningsReported;
        _hardware.Dispose();
        _hardware = null;
	}

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Driver Class Overrides
    //

    public override void SerializeStateForClient(Guid sessionId, XmlWriter writer)
    {
        var update = new SyncNodeUpdate();
        foreach (LutronButton button in _mapPathToButton.Values)
        {
            update.Path = button.Path;
            update.Value = NullIfDisabled(button.LedIsOn);
            update.Serialize(writer, false);
        }
    }

    public override void ProcessSyncNodeCommand(Guid originator, SyncNodeCommand command, MessageBuilder replyBuilder)
    {
        LutronButton button;
        if (_mapPathToButton.TryGetValue(command.Path, out button))
        {
            if (command.Verb != "Press")
                throw new SyncException("Invalid verb \"{0}\"; the only valid verb is \"Press\"", command.Verb);

            // zz

            List<bool> waitingForLedStates;
            switch (button.LedBehavior)
            {

            case LedBehavior.Light:

                // the caller is pressing a button whose LED turns on when lit
                if (button.LedIsOn)
                {
                    // the LED is already on, so there's nothing to wait for
                    waitingForLedStates = null;
                }
                else
                {
                    // wait for the LED to turn on
                    waitingForLedStates = new List<bool>() { true };
                }
                break;

            case LedBehavior.Extinguish:

                // the caller is pressing a button whose LED turns off when lit
                if (!button.LedIsOn)
                {
                    // the LED is already off, so there's nothing to wait for
                    waitingForLedStates = null;
                }
                else
                {
                    // wait for the LED to turn off
                    waitingForLedStates = new List<bool>() { false };
                }
                break;

            case LedBehavior.Toggle:

                // the caller is pressing a button whose LED toggles, i.e. turns off if it's
                // currently on and vice versa
                waitingForLedStates = new List<bool>() { !button.LedIsOn };
                break;

            case LedBehavior.Cycle:

                // the caller is pressing a button that behaves as follows:
                //   -- if it's initially off, it will turn on, stay on for a bit, then turn off
                //   -- if it's initially on, it will turn off
                if (!button.LedIsOn)
                {
                    // the LED is initially off
                    waitingForLedStates = new List<bool>() { true, false };
                }
                else
                {
                    // the LED is initially on
                    waitingForLedStates = new List<bool>() { false };
                }
                break;

            default:

                throw new SyncException(
                    "No behavior is specified in the .config file for the LED of button \"{0}\"",
                    button.Address);

            }

            var request = new LutronKeypadButtonPressRequest()
            {
                Driver = this,
                Button = button,
			};
            request.OverrideOnExit = delegate
            {
                DoneCurrentRequest(request);
                if (request.Error != null)
                    waitingForLedStates = null;
                ContinueKeypadButtonPressRequest(originator, command, button,
                    waitingForLedStates);
            };
            QueueRequest(request, false);
        }
        else
            throw new SyncException("Invalid SyncPath \"{0}\" (command \"{1}\")", command.Path, command.Verb);
    }

    void ContinueKeypadButtonPressRequest(Guid originator, SyncNodeCommand command, LutronButton button, List<bool> waitingForLedStates)
    {
        // at this point, the Lutron "KBP" command has been sent to the hardware, and the hardware
        // has responded with a corresponding "KBP" notification -- now we need to wait until the
        // LED on the keypad button changes to the series of states specified by
        // <waitingForLedStates> -- or, if <waitingForLedStates> is null, this request is complete

        // if we're not waiting for any keypad button LED state changes, end this request now
        if ((waitingForLedStates == null) || (waitingForLedStates.Count == 0))
        {
            BroadcastSyncNodeOperation(command, originator);
            return;
        }

        // cancel any SecondaryKeypadButtonPressRequest requests that are for the same SyncPath as
        // this request
        LazyList<LutronParallelRequest> requestsToCancel = new LazyList<LutronParallelRequest>();
        foreach (LutronParallelRequest existingRequest in _parallelRequests)
        {
            var specificRequest = existingRequest as SecondaryKeypadButtonPressRequest;
            if ((specificRequest != null) && (specificRequest.SyncNode.Path == command.Path))
                requestsToCancel.Add(specificRequest);
        }
        while (requestsToCancel.Count > 0)
        {
            requestsToCancel[0].OnExit();
            requestsToCancel.RemoveAt(0);
        }

        // create a new SecondaryKeypadButtonPressRequest to wait for LED state change(s)
        var request = new SecondaryKeypadButtonPressRequest()
        {
            Driver = this,
			Originator = originator,
            SyncNode = (SyncNode)PseudoClientTree.CreateNode(command.Path),
            WaitingForLedStates = waitingForLedStates
        };
        request.OnInitialize();
    }

    List<LutronParallelRequest> _parallelRequests = new List<LutronParallelRequest>();

    /// <summary>
    /// An operation that can execute in parallel with other <r>ParallelRequest</r> operations (unlike
    /// <r>LutronRequest</r>, for which one request must complete before another can begin).
    /// </summary>
    abstract class LutronParallelRequest
    {
        public LutronDriver Driver;
        public Guid Originator;

        public abstract void OnInitialize();
        public abstract void OnExit();
    }

    /*
    When a LutronKeypadButtonPressRequest completes (as a result of a command being sent to the
    hardware and the hardware echoing back the command), then if we need to wait for keypad LED
    states to change in order for the overall request to be considered complete, a
    SecondaryKeypadButtonPressRequest is created.  Unlike LutronKeypadButtonPressRequest, which
    is serialized (the hardware executes one such command at a time), multiple
    SecondaryKeypadButtonPressRequest reqests can exist in parallel -- each is waiting for
    a particular LED state change sequence to complete before the overall request can be considered
    complete.
    */
    class SecondaryKeypadButtonPressRequest : LutronParallelRequest
    {
        public List<bool> WaitingForLedStates;
        //public SyncNodeCommand Command;
        public SyncNode SyncNode;

        public override void OnInitialize()
        {
            //zz
            SyncNode.ValueChanged += SyncNode_ValueChanged;
            Driver._parallelRequests.Add(this);
            Driver.BusyCount++;
        }

        public override void OnExit()
        {
            SyncNode.ValueChanged -= SyncNode_ValueChanged;
            Driver._parallelRequests.Remove(this);
            Driver.BusyCount--;
            Driver.BroadcastSyncNode(SyncNode, Originator);
        }

        public void SyncNode_ValueChanged(object sender, ValueChangedEventArgs e)
        {
            // the state of the keypad button LED that we're monitoring has changed -- if it's
            // changed to the next state we're waiting for (as specified in WaitingForLedStates),
            // move to the next state in WaitingForLedStates, or end this request if there are
            // no more states
            bool newValue = (bool)e.NewValue;
            if (newValue == WaitingForLedStates[0])
            {
                WaitingForLedStates.RemoveAt(0);
                if (WaitingForLedStates.Count == 0)
                    OnExit();
            }
        }
    }

	///////////////////////////////////////////////////////////////////////////////////////////////
	// IDisposable Overrides
	//

    public override void Dispose()
    {
        if (_hardware != null)
        {
            _hardware.Dispose();
            _hardware = null;
        }
        base.Dispose();
    }

	///////////////////////////////////////////////////////////////////////////////////////////////
	// Helper Methods
	//

#if false // TODO -- do we need these methods?

    protected override void MergeRequests(IList<Driver.DriverRequest> futureRequests)
    {
        while (true)
        {
            if (futureRequests.Count < 2)
                return;

            var first = futureRequests[0] as LutronOutputVolumeChangeRequest;
            if (first == null)
                return;

            LutronOutputVolumeChangeRequest other = FindRequestToMergeInto(futureRequests, first);
            if (other != null)
            {
                other.Originators.AddRange(first.Originators);
                futureRequests.RemoveAt(0);
            }
            else
                break;
        }
    }

    LutronOutputVolumeChangeRequest FindRequestToMergeInto(IList<Driver.DriverRequest> futureRequests,
        LutronOutputVolumeChangeRequest first)
    {
        for (int requestIndex = 1; requestIndex < futureRequests.Count; requestIndex++)
        {
            var other = futureRequests[requestIndex] as LutronOutputVolumeChangeRequest;
            if (other == null)
                return null;
            if (other.Output == first.Output)
                return other;
        }

        return null;
    }

#endif

    string[] GetProcessorNames()
    {
        string[] result = new string[99/*TODO*/];
        for (int index = 0; index < result.Length; index++)
        {
            if (index == 0)
                result[index] = "Processor";
            else
                result[index] = String.Format("Processor {0}", index + 1);
        }

        return result;
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    //

	public void OnHardwareCommandFailed(object sender, LutronHardwareCommandFailedEventArgs e)
	{
		ReportAlert(MessageAlertType.Warning, "\"{0}\" command failed", e.Command);
	}

	public void OnHardwareCommandTimedOut(object sender, SimpleSerialHardwareCommandTimedOutEventArgs e)
	{
        ReportAlert(MessageAlertType.Warning, "\"{0}\" command timed out", e.Command);
	}

	public void OnHardwareNotificationReceived(object sender, LutronHardwareNotificationReceivedEventArgs e)
	{
        // ignore notifications received from the hardware while we're in the process of enabling
        // the hardware -- however, note that the very last notification may be received from
        // <_hardware> asynchronously, after enabling is complete (but it shouldn't hurt anything,
        // because the driver state will already reflect what the notification says)
        if (!IsEnabled)
            return;

        // if we come across miscellaneous issue(s) to report (e.g. the driver sends text we
        // don't expect), <warnings> will be set to the issue(s) to report, otherwise it
        // will be null
        LazyStringList warnings = new LazyStringList();

        foreach (LutronNotification lutronNotification in e.Notifications)
        {
            switch (lutronNotification.Kind)
            {
			case LutronNotificationKind.DimmerLevelChange:
				break;
			case LutronNotificationKind.GrafikEyeSceneChange:
				break;
			case LutronNotificationKind.KeypadButtonDoubleTap:
				break;
			case LutronNotificationKind.KeypadButtonHold:
				break;
			case LutronNotificationKind.KeypadButtonPress:
				break;
			case LutronNotificationKind.KeypadLedStateChange:
                var notification = (LutronKeypadLedStateChangeNotification)lutronNotification;
                LutronKeypad keypad;
                if (!_mapAddressToKeypad.TryGetValue(notification.Address, out keypad))
                {
                    warnings.Add(
                        "LutronHardware reported keypad LED state change for unknown keypad: {0}",
                        notification.Address);
                    break;
                }
                for (int buttonId = 1; buttonId <= notification.LedStates.Length; buttonId++)
                {
                    LutronButton button;
                    if (keypad.MapIdToButton.TryGetValue(buttonId, out button))
                    {
                        bool ledIsOn = notification.LedStates[buttonId - 1];
                        if (button.LedIsOn != ledIsOn)
                        {
                            button.LedIsOn = ledIsOn;
                            var update = new SyncNodeUpdate()
                            {
                                Path = button.Path,
                                Value = ledIsOn
                            };
                            BroadcastMessageElements(
                                delegate(Guid sessionId, MessageBuilder messageBuilder)
                                {
                                    update.Serialize(messageBuilder.Writer, false);
                                });
                        }
                    }
                }
				break;
            }
        }

        // report warnings, if any
        ReportWarnings(warnings);
        
        
        // if driver state differs from <e> *and* we're enabled, broadcast to clients


		BroadcastMessageElements(delegate(Guid sessionId, MessageBuilder messageBuilder)
		{
			foreach (LutronNotification notification in e.Notifications)
			{
			}
		});
	}

    public void ReportWarnings(LazyStringList warnings)
    {
        if (warnings.Count > 0)
        {
            foreach (string warning in warnings)
                ReportAlert(MessageAlertType.Warning, warning);
        }
    }

	public void OnHardwareWarningsReported(object sender, LutronHardwareWarningsReportedEventArgs e)
	{
		foreach (string message in e.Messages)
            ReportAlert(MessageAlertType.Warning, message);
	}

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Nested Classes
    //

	/// <summary>
	/// Represents a Lutron processor.
	/// </summary>
	[DebuggerDisplay("LutronProcessor {Id}")]
	class LutronProcessor
	{
        /// <summary>
        /// The parent <r>LutronDriver</r>, i.e. the <r>LutronDriver</r> that contains this
        /// processor.
        /// </summary>
        public LutronDriver Driver;

        /// <summary>
        /// The identifier of this processor.  This is the 1-based number of the processor within
		/// the Lutron system.
        /// </summary>
		public int Id;

		/// <summary>
		/// Maps the 1-based link identifier of each link to the corresponding <r>LutronLink</r>
		/// object.
		/// </summary>
		public Dictionary<int, LutronLink> MapIdToLink = new Dictionary<int, LutronLink>();

		/// <summary>
		/// The list of links associated with this processor.  Unused links are not included.
		/// The key is the 1-based link number (the "Id" attribute on the "&lt;*Link&gt; element
		/// in the configuration file).
		/// </summary>
        public Dictionary<int, LutronLink> Links = new Dictionary<int, LutronLink>();

        /// <summary>
        /// Finds a <r>LutronLink</r> in <r>Links</r> given a link ID.
        /// </summary>
        ///
        /// <param name="linkId">The link ID to look for.</param>
        ///
        /// <returns>
        /// The <r>LutronLink</r>.
        /// </returns>
        /// 
        /// <exception cref="DriverException">
        /// <pr>linkId</pr> doesn't correspond to a known Lutron link within this processor.
        /// </exception>
        ///
        public LutronLink FindLink(int linkId)
        {
            try
            {
                return Links[linkId];
            }
            catch (KeyNotFoundException)
            {
                throw new DriverException("Lutron processor {0} has no link {1}", Id, linkId);
            }
        }

        /// <summary>
        /// Finds a <r>LoadLink</r> in <r>Links</r> given a link ID.
        /// </summary>
        ///
        /// <param name="linkId">The link ID to look for.</param>
        ///
        /// <returns>
        /// The <r>LoadLink</r>.
        /// </returns>
        /// 
        /// <exception cref="DriverException">
        /// <pr>linkId</pr> doesn't correspond to a known Lutron load link within this processor.
        /// </exception>
        ///
        public LoadLink FindLoadLink(int linkId)
        {
            LutronLink link = FindLink(linkId);
            LoadLink loadLink = link as LoadLink;
            if (loadLink != null)
                return loadLink;
            else
                throw new DriverException("Lutron link {0} is not a load link", link.Address);
        }

        /// <summary>
        /// Finds a <r>KeypadLink</r> in <r>Links</r> given a link ID.
        /// </summary>
        ///
        /// <param name="linkId">The link ID to look for.</param>
        ///
        /// <returns>
        /// The <r>KeypadLink</r>.
        /// </returns>
        /// 
        /// <exception cref="DriverException">
        /// <pr>linkId</pr> doesn't correspond to a known Lutron keypad link within this processor.
        /// </exception>
        ///
        public KeypadLink FindKeypadLink(int linkId)
        {
            LutronLink link = FindLink(linkId);
            KeypadLink keypadLink = link as KeypadLink;
            if (keypadLink != null)
                return keypadLink;
            else
                throw new DriverException("Lutron link {0} is not a keypad link", link.Address);
        }

        /// <summary>
        /// Enumerates the Lutron hardware elements on this <r>LutronProcessor</r>.
        /// </summary>
        public IEnumerable<LutronHardwareElement> HardwareElements
        {
            get
            {
				foreach (LutronLink link in Links.Values)
				{
					foreach (LutronHardwareElement hardwareElement in link.HardwareElements)
					{
						yield return hardwareElement;
						LutronKeypad lutronKeypad = hardwareElement as LutronKeypad;
						if (lutronKeypad != null)
						{
							foreach (LutronButton button in lutronKeypad.MapIdToButton.Values)
								yield return button;
						}
					}
				}
            }
        }
    }

	/// <summary>
	/// Represents a link between a Lutron processor and connected hardware.  The derived class
	/// indicates the type of hardware: <r>KeypadLink</r>, <r>RpmLink</r>, or <r>VareoLink</r>.
	/// </summary>
	[DebuggerDisplay("LutronLink {Address}")]
    abstract class LutronLink
    {
        /// <summary>
        /// The processor that contains this link.
        /// </summary>
        public LutronProcessor Processor;

        /// <summary>
        /// The identifier of this button.  This is the 1-based number of the link within the
		/// parent <r>LutronProcessor</r>.
        /// </summary>
		public int Id;

        /// <summary>
        /// The fully-qualified address of this link.  This is a two-part dotted address of
		/// the form "<i>processor</i>.<i>link</i>"; for example, "1.4".
        /// </summary>
        public string Address
        {
			get
			{
                return String.Format("{0}.{1}", Processor.Id, Id);
			}
        }

		/// <summary>
        /// Enumerates the Lutron hardware elements on this <r>LutronLink</r>.
		/// </summary>
        public abstract IEnumerable<LutronHardwareElement> HardwareElements { get; }
    }

	/// <summary>
	/// Represents a <r>LutronLink</r> which is connected to a series of keypads.
	/// </summary>
	[DebuggerDisplay("KeypadLink {Address}")]
    class KeypadLink : LutronLink
    {
		/// <summary>
		/// Maps the 1-based keypad identifier of each keypad on this link to the corresponding
        /// <r>LutronKeypad</r> object.
		/// </summary>
		public Dictionary<int, LutronKeypad> MapIdToKeypad = new Dictionary<int, LutronKeypad>();

        /// <summary>
        /// Finds a <r>LutronKeypad</r> in <r>Keypads</r> given a keypad ID.
        /// </summary>
        ///
        /// <param name="keypadId">The keypad ID to look for.</param>
        ///
        /// <returns>
        /// The <r>LutronKeypad</r>.
        /// </returns>
        /// 
        /// <exception cref="DriverException">
        /// <pr>keypadId</pr> doesn't correspond to a known Lutron keypad within this link.
        /// </exception>
        ///
        public LutronKeypad FindKeypad(int keypadId)
        {
            try
            {
                return MapIdToKeypad[keypadId];
            }
            catch (KeyNotFoundException)
            {
                throw new DriverException("Lutron keypad link {0} has no keypad {1}", Address,
					keypadId);
            }
        }

        /// <summary>
        /// Enumerates the Lutron hardware elements on this <r>LutronLink</r>.
        /// </summary>
        public override IEnumerable<LutronHardwareElement> HardwareElements
        {
            get
            {
                foreach (LutronKeypad keypad in MapIdToKeypad.Values)
                    yield return keypad;
            }
        }
    }

	/// <summary>
	/// Represents a <r>LutronLink</r> which is connected to a set of loads.  The derived class is
	/// either <r>RpmLink</r> or <r>VareoLink</r>.
	/// </summary>
	[DebuggerDisplay("LoadLink {Address}")]
    abstract class LoadLink : LutronLink
    {
		/// <summary>
		/// Maps the 3-part identifier of each load in this link to the corresponding
		/// <r>LutronLoad</r> object.  The key is of the form
		/// "<i>interface-number</i>.<i>bus/module-number</i>.<i>dimmer-number</i>.".
		/// </summary>
		public Dictionary<string, LutronLoad> MapIdToLoad =
			new Dictionary<string, LutronLoad>();

        /// <summary>
        /// Finds a <r>LutronLoad</r> in <r>Loads</r> given a load ID.
        /// </summary>
        ///
        /// <param name="loadId">The load ID to look for.</param>
        ///
        /// <returns>
        /// The <r>LutronLoad</r>.
        /// </returns>
        /// 
        /// <exception cref="DriverException">
        /// <pr>loadId</pr> doesn't correspond to a known Lutron load within this link.
        /// </exception>
        ///
        public LutronLoad FindLoad(string loadId)
        {
            try
            {
                return MapIdToLoad[loadId];
            }
            catch (KeyNotFoundException)
            {
                throw new DriverException("Lutron load link {0} has no load {1}", Address, loadId);
            }
        }

        /// <summary>
        /// Enumerates the Lutron hardware elements on this <r>LutronLink</r>.
        /// </summary>
        public override IEnumerable<LutronHardwareElement> HardwareElements
        {
            get
            {
                foreach (LutronLoad load in MapIdToLoad.Values)
                    yield return load;
            }
        }
    }

	/// <summary>
	/// Represents a <r>LutronLink</r> which is connected to an RPM (remote power module).
	/// </summary>
	[DebuggerDisplay("RpmLink {Address}")]
    class RpmLink : LoadLink
    {
    }

	/// <summary>
	/// Represents a <r>LutronLink</r> which is connected to a series of Vareos (dimmer switches).
	/// </summary>
	[DebuggerDisplay("VareoLink {Address}")]
    class VareoLink : LoadLink
    {
    }

	/// <summary>
	/// Represents a piece of hardware connected to the Lutron system that can be controlled;
	/// examples: keypads, keypad buttons, loads.
	/// </summary>
	abstract class LutronHardwareElement
	{
	}

	/// <summary>
	/// Represents a Lutron keypad, or a device that simulates a keypad such as a contact closure
	/// input (CCI) board, which acts like a keypad with buttons that represent the states of
	/// inputs and no LEDs, or a contact closure outut (CCO) board, which acts like a keypad with
	/// with only LEDs (which represent relays) and no buttons.
	/// </summary>
	[DebuggerDisplay("LutronKeypad {Address} ({Name})")]
	class LutronKeypad : LutronHardwareElement
	{
        /// <summary>
        /// The <r>KeypadLink</r> that owns this object.
        /// </summary>
        public KeypadLink Link;

        /// <summary>
        /// The identifier of this button.  This is the 1-based number of the keypad within the
		/// parent <r>KeypadLink</r>.
        /// </summary>
		public int Id;

		/// <summary>
		/// The name of the keypad, specified by the "Name" attribute on the "&lt;Keypad&gt;"
		/// element.
		/// </summary>
		public string Name;

        /// <summary>
        /// Maps the 1-based button identifier of each button to the corresponding <r>LutronButton</r>
        /// object.  For a keypad that's really a CCI or CCO board, the <r>LutronButton</r> really
        /// represents a contact closure input or relay, respectively.
        /// </summary>
        public Dictionary<int, LutronButton> MapIdToButton = new Dictionary<int, LutronButton>();

        /// <summary>
        /// The fully-qualified address of this keypad.  This is a three-part dotted address of
		/// the form "<i>processor</i>.<i>link</i>.<i>address</i>"; for example, "1.4.10".
        /// </summary>
        public string Address
        {
			get
			{
                return String.Format("{0}.{1}.{2}", Link.Processor.Id, Link.Id, Id);
			}
        }

        public SyncPath Path
        {
            get
            {
                return SyncPath.Build(Link.Processor.Driver.Name, "Keypads", Name);
            }
        }

        /// <summary>
        /// Finds a <r>LutronButton</r> in <r>Buttons</r> given a button ID.
        /// </summary>
        ///
        /// <param name="buttonId">The button ID to look for.  This is the 1-based button number.
		/// 	</param>
        ///
        /// <returns>
        /// The <r>LutronButton</r>.
        /// </returns>
        /// 
        /// <exception cref="DriverException">
        /// <pr>buttonId</pr> doesn't correspond to a known Lutron button within this link.
        /// </exception>
        ///
        public LutronButton FindButton(int buttonId)
        {
            try
            {
                return MapIdToButton[buttonId];
            }
            catch (KeyNotFoundException)
            {
                throw new DriverException("Lutron keypad \"{0}\" ({1}) has no button {2}",
					Name, Address, buttonId);
            }
        }
    }

	/// <summary>
	/// Represents a button on a Lutron keypad, or a contact closure inputs or output on a
	/// keypad-like device like a CCI or CCO board.
	/// </summary>
	[DebuggerDisplay("LutronButton {Address}")]
	class LutronButton : LutronHardwareElement
	{
        /// <summary>
        /// The <r>LutronKeypad</r> that owns this object.
        /// </summary>
        public LutronKeypad Keypad;

        /// <summary>
        /// The identifier of this button.  This is the 1-based number of the button on the
		/// <r>LutronKeypad</r>.
        /// </summary>
		public int Id;

		/// <summary>
		/// Defines how a button's LED behaves when the button is pressed.
		/// </summary>
		public LedBehavior LedBehavior;

		/// <summary>
		/// <n>true</n> if (as far as we know) the LED associated with the button is on.
		/// </summary>
		public bool LedIsOn;

        /// <summary>
        /// The fully-qualified address of this load.  This is a four-part dotted address of the
		/// form "<i>processor</i>.<i>link</i>.<i>address</i>.<i>button</i>"; for example,
		/// "1.4.10.7".
        /// </summary>
        public string Address
        {
            get
            {
                return String.Format("{0}.{1}", Keypad.Address, Id);
            }
        }

        public SyncPath Path
        {
            get
            {
                return SyncPath.Build(Keypad.Path, "Buttons", XmlConvert.ToString(Id));
            }
        }
    }

	/// <summary>
	/// Represents a Lutron Lutron load, i.e. some device (such as a light) controlled by
	/// one RPM dimmer or one Vareo.
	/// </summary>
	[DebuggerDisplay("LutronLoad {Address} ({Name})")]
	class LutronLoad : LutronHardwareElement
	{
        /// <summary>
        /// The <r>LoadLink</r> that contains this load.
        /// </summary>
        public LoadLink Link;

        /// <summary>
        /// The identifier of this load.  This is a three-part dotted address of the load within
		/// the <r>LoadLink</r>.  The address is of the form
		/// "<i>interface</i>.<i>module/bus</i>.<i>dimmer</i>"; for example, "1.7.3".
		/// </summary>
		public string Id;

        /// <summary>
        /// The load of the keypad, specified by the "Name" attribute on the "&lt;Load&gt;"
		/// element.
		/// </summary>
		public string Name;

		/// <summary>
		/// The current level (as far as we know) of the level, as a percentage from 0 (off) to
		/// 100 (fully on).
		/// </summary>
		public int Level;

        /// <summary>
        /// The fully-qualified address of this load.  This is a five-part dotted address of the
		/// form "<i>processor</i>.<i>link</i>.<i>interface</i>.<i>module/bus</i>.<i>dimmer</i>";
		/// for example, "1.5.1.7.3".
        /// </summary>
        public string Address
        {
			get
			{
                return String.Format("{0}.{1}.{2}", Link.Processor.Id, Link.Id, Id);
            }
        }

        /// <summary>
        /// Creates a string <r>LutronLoad</r> ID given the three integer parts of the ID.
        /// </summary>
		///
        /// <param name="interfaceId">The interface ID.</param>
		///
        /// <param name="busOrModuleId">The bus or module ID.</param>
		///
        /// <param name="dimmerId">The dimmer ID.</param>
		///
        static public string MakeId(int interfaceId, int busOrModuleId, int dimmerId)
		{
			return String.Format("{0}.{1}.{2}", interfaceId, busOrModuleId, dimmerId);
		}
    }

	/// <summary>
	/// Defines how a button's LED behaves when the button is pressed.
	/// </summary>
	internal enum LedBehavior
	{
		/// <summary>
		/// The behavior is unknown.  HomeUX will assume the button action is complete immediately,
		/// ignoring the state of the LED.  Corresponds to button code "?" in the "Buttons"
		/// attribute of the "&lt;Keypad&gt;" element in the configuration XML.
		/// </summary>
		Unknown,

		/// <summary>
		/// The button's LED lights (turns on) when the button action is complete.  If you press
		/// the button again, it will stay lit.  Corresponds to button code "L" in the "Buttons"
		/// attribute of the "&lt;Keypad&gt;" element in the configuration XML.
		/// </summary>
		Light,

		/// <summary>
		/// The button's LED extinguishes (turns off) when the button action is complete.  If you
		/// press the button again, it will stay turned off.  Corresponds to button code "E" in
		/// the "Buttons" attribute of the "&lt;Keypad&gt;" element in the configuration XML.
		/// </summary>
		Extinguish,

		/// <summary>
		/// The button toggles (turns off if it was initially on, turns on if it was initially off)
		/// when the button action is complete.  Corresponds to button code "T" in the "Buttons"
		/// attribute of the "&lt;Keypad&gt;" element in the configuration XML.
		/// </summary>
		Toggle,

		/// <summary>
		/// The button cycles (turns on, then turns off, typically a few seconds later) when the
		/// button action is complete.  Corresponds to button code "C" in the "Buttons" attribute
		/// of the "&lt;Keypad&gt;" element in the configuration XML.
		/// </summary>
		Cycle,
	}

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // LutronRequest and Derived Types
    //

    abstract class LutronRequest : Driver.Request
    {
        protected new LutronDriver Driver
        {
            [DebuggerStepThrough]
            get
            {
                return (LutronDriver)base.Driver;
            }
        }
    }

    [DebuggerDisplay("Enable driver")]
    class LutronEnableRequest : LutronRequest, Driver.IEnableRequest
    {
        protected override IEnumerable<bool> Implementation()
        {
            // open the hardware
            try
            {
                Driver.OpenHardware();
            }
            catch (VirtualSerialPortException ex)
            {
                Error = Util.FormatExceptionMessage(ex);
                yield break; // end LutronRequest
            }

            // query the hardware for its full status (e.g. which lights are on)

            foreach (LutronKeypad keypad in Driver._mapNameToKeypad.Values)
            {
                var requestKeypadLedStates = new ResultHolder<RequestKeypadLedStatesReply>(this);
                Driver._hardware.RequestKeypadLedStates(keypad.Address, requestKeypadLedStates.OnDone);
                yield return true; // wait for async reply
                if (requestKeypadLedStates.Result == null)
                {
                    Error = "RequestKeypadLedStates failed";
                    yield break; // end LutronRequest
                }

                bool[] ledStates = requestKeypadLedStates.Result.LedStates;
                for (int buttonId = 1; buttonId <= ledStates.Length; buttonId++)
                {
                    LutronButton button;
                    if (keypad.MapIdToButton.TryGetValue(buttonId, out button))
                        button.LedIsOn = ledStates[buttonId - 1];
                }
            }
        }
    }

    /*
    This request ends when the hardware has echoed back the command -- not when
    the LED states have changed (which happens later).  This request is serialized
    (along with all other LutronRequest objects) -- however, when this request is
    done, ContinueKeypadButtonPressRequest is called, which causes a
    SecondaryKeypadButtonPressRequest to begin.
    */
    [DebuggerDisplay("LutronKeypadButtonPressRequest: {ButtonAddress}")]
    class LutronKeypadButtonPressRequest : LutronRequest
    {
        /// <summary>
        /// The button to press.
        /// </summary>
        public LutronButton Button;

        protected override IEnumerable<bool> Implementation()
        {
            var pressKeypadButton = new ResultHolder<GenericReply>(this);
			Driver.Trace(true, "HW: Press button {0} ({1} #{2})",
                Button.Address, Button.Keypad.Name, Button.Id);
            Driver._hardware.PressKeypadButton(Button.Address, pressKeypadButton.OnDone);
            yield return true; // wait for async reply
            if (pressKeypadButton.Result == null)
            {
                Error = "PressKeypadButton failed";
                yield break; // end LutronRequest
            }
        }
    }
}

}

